public class RabbitMQ笔记 {
/*
RabbitMQ基本使用

同步调用的优点：
- 时效性较强，可以立即得到结果

同步调用的问题：
- 耦合度高
- 性能和吞吐能力下降
- 有额外的资源消耗
- 有级联失败问题

异步通信:
好处：
- 吞吐量提升：无需等待订阅者处理完成，响应更快速
- 故障隔离：服务没有直接调用，不存在级联失败问题
- 调用间没有阻塞，不会造成无效的资源占用
- 耦合度极低，每个服务都可以灵活插拔，可替换
- 流量削峰：不管发布事件的流量波动多大，都由Broker接收，订阅者可以按照自己的速度去处理事件

缺点：
- 架构复杂了，业务没有明显的流程线，不好管理
- 需要依赖于Broker的可靠、安全、性能



MQ，中文是消息队列（MessageQueue），字面来看就是存放消息的队列。用于服务间异步通信,也就是事件驱动架构中的Broker。
比较常见的MQ实现：
- ActiveMQ
- RabbitMQ
- RocketMQ
- Kafka

几种常见MQ的对比：

|            | **RabbitMQ**            | **ActiveMQ**                   | **RocketMQ** | **Kafka**  |
| ---------- | ----------------------- | ------------------------------ | ------------ | ---------- |
| 公司/社区  | Rabbit                  | Apache                         | 阿里         | Apache     |
| 开发语言   | Erlang                  | Java                           | Java         | Scala&Java |
| 协议支持   | AMQP，XMPP，SMTP，STOMP | OpenWire,STOMP，REST,XMPP,AMQP | 自定义协议   | 自定义协议 |
| 可用性     | 高                      | 一般                           | 高           | 高         |
| 单机吞吐量 | 一般                    | 差                             | 高           | 非常高     |
| 消息延迟   | 微秒级                  | 毫秒级                         | 毫秒级       | 毫秒以内   |
| 消息可靠性 | 高                      | 一般                           | 高           | 一般       |



RabbitMQServer结构示意图.png


RabbitMQ中的几个概念：

- channel：操作MQ的工具
- publisher：生产者
- consumer：消费者
- exchange：交换机，负责消息路由
- queue：队列，存储消息
- virtualHost：虚拟主机，隔离不同租户的exchange、queue、消息的隔离





常见消息模型

无交换机:
Basic Queue 简单队列模型
Work Queue 工作队列模型

有交换机:
发布(Publish)、订阅(Subscribe)模型 - Fanout广播
发布(Publish)、订阅(Subscribe)模型 - Direct路由
发布(Publish)、订阅(Subscribe)模型 - Topic主题



基本消息队列的消息发送流程：

1. 建立connection
2. 创建channel
3. 利用channel声明队列queue
4. 利用channel向队列发送消息

基本消息队列的消息接收流程：

1. 建立connection
2. 创建channel
3. 利用channel声明队列queue
4. 定义consumer的消费行为handleDelivery()
5. 利用channel将消费者与队列绑定

之所以生产者和消费者都要声明队列,是因为生产消费是异步执行的,先后顺序是不确定的,为了避免队列不存在




AMQP(Advanced Message Queuing Protocol)高级消息队列协议,是用于在应用程序之间传递业务消息的开放标准.该协议与语言和平台无关,更符合微服务中独立性的要求.

Spring AMQP 是基于AMQP协议定义的一套API规范,提供了模板来发送和接受消息.包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现.



BasicQueue简单队列模型:
SpringAMQP如何发送消息？
1.引入amqp的starter依赖
2.配置RabbitMQ地址
3.注入RabbitTemplate并利用rabbitTemplate.convertAndSend(queueName, message)方法发送消息

SpringAMQP如何接收消息？
1.引入amqp的starter依赖
2.配置RabbitMQ地址
3.在任意能被@SpringBootApplication扫描到的包中定义监听类(可以新建listener包),并添加@Component注解,类中声明监听回调方法，添加@RabbitListener注解，方法形参形参对应消息队列中的消息
注意：消息一旦消费就会从队列删除，RabbitMQ没有消息回溯功能


WorkQueue(工作队列)模型的使用:
与简单队列模型一样,只不过有多个消费者合作共同处理消息队列
多个消费者绑定到一个队列，同一条消息只会被一个消费者处理
通过设置prefetch来控制消费者预取的消息数量
listener:
  simple:
    prefetch: 1 #预取值,每次只能得到一条消息，处理完成ACK之后，才能获取下一个消息,实现能者多劳,处理速度快的消费者多消费一些消息,不设置预取值,WorkQueue将会平均分配消息给消费者,导致速度快的消费者早早结束,速度慢的消费者消息堆积



发布（ Publish ）、订阅（ Subscribe ）
发布订阅模式与之前无交换机的简单工作队列的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange（交换机）。
常见exchange(交换机)类型包括：
Fanout：广播(扇出)
Direct：路由
Topic：话题
注意：Exchange（交换机）只负责转发消息，不具备存储消息的能力，路由失败则消息丢失.如果没有任何队列与Exchange绑定，或者没有符合路由规则的队列，那么消息会丢失！



Fanout Exchange 会将接收到的消息广播到每一个跟其绑定的queue
在广播模式下，消息发送流程是这样的：
- 1）  可以有多个队列
- 2）  每个队列都要绑定到Exchange（交换机）
- 3）  生产者发送的消息，只能发送到交换机，交换机来决定要发给哪个队列，生产者无法决定
- 4）  交换机把消息发送给绑定过的所有队列
- 5）  订阅队列的消费者都能拿到消息

实现思路如下：
在consumer服务中，利用代码声明队列Queue、交换机FanoutExchange，并将两者绑定Binding
在consumer服务中，编写两个消费者方法，分别监听fanout.queue1和fanout.queue2
在publisher中编写测试方法，向itcast.fanout发送消息

### 总结
交换机的作用是什么？
- 接收publisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- 不能缓存消息，路由失败，消息丢失
- FanoutExchange的会将消息路由到每个绑定的队列


Direct Exchange 会将接收到的消息根据规则路由到指定的Queue，因此称为路由模式（routes）。
每一个Queue绑定Exchange时会设置一个bindingkey(一个队列可以同时指定多个bindingkey,不同队列可以有相同的bindingkey)
发布者发送消息时，指定消息的RoutingKey
Exchange路由消息到queue时,根据bindingkey与RoutingKey相一致为原则

实现思路如下：
在SpringRabbitListener类中,利用@RabbitListener以注解的形式创建Exchange,Queue并绑定,以及声明bindingkey,同时声明队列direct.queue1和direct.queue2的监听回调函数
在publisher服务的SpringAmqpTest类中添加测试方法发送消息到交换机itcast.direct

@RabbitListener(
    bindings = @QueueBinding(
        value = @Queue(name = "direct.queue1"),
        exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),不写交换机的type属性,默认是direct类型
        key = {"red","blue"}
    )
)//以注解的形式创建Exchange,Queue并绑定,以及声明bindingkey,同时声明队列监听,回调函数
public void listenDirectQueue1(String message) {
    System.out.println("消费者接收到direct.queue1的消息:[" + message + "]");
}
//direct.queue2的监听回调函数listenDirectQueue2类似...

### 总结
描述下Direct交换机与Fanout交换机的差异？
1.Fanout交换机将消息路由给每一个与之绑定的队列
2.Direct交换机根据RoutingKey == bindingkey判断路由给哪个队列
3.如果多个队列具有相同的bindingKey，则与Fanout功能类似



TopicExchange`类型的`Exchange`与`Direct`相比，都是可以根据`RoutingKey`把消息路由到不同的队列。只不过`Topic`类型`Exchange`可以让队列在绑定`bindingkey` 的时候使用通配符！
Routingkey` 一般都是有一个或多个单词组成，多个单词之间以”.”分割，例如： `item.insert`
通配符规则：
#：代指0个或多个单词
*：代指一个单词
举例：
`item.#`：能够匹配`item.spu.insert` 或者 `item.spu`
`item.*`：只能匹配`item.spu`
Queue1：绑定的是`china.#` ，因此凡是以 `china.`开头的`routing key` 都会被匹配到。包括china.news和china.weather
Queue2：绑定的是`#.news` ，因此凡是以 `.news`结尾的 `routing key` 都会被匹配。包括china.news和japan.news

多级的routingkey代表着一种话题,因此才叫TopicExchange话题交换机
china.news 代表有中国的新闻消息；
china.weather 代表中国的天气消息；
japan.news 则代表日本新闻
japan.weather 代表日本的天气消息；

实现思路如下：
在SpringRabbitListener类中,利用@RabbitListener以注解的形式创建Exchange,Queue并绑定,以及声明bindingkey(china.# 、#.news),同时声明队列topic.queue1和topic.queue2的监听回调函数
在publisher服务的SpringAmqpTest类中添加测试方法发送消息到交换机itcast.topic


###总结
描述下Direct交换机与Topic交换机的差异？
Topic交换机接收的消息RoutingKey必须是多个单词，以 . 分割
Topic交换机与队列绑定时的bindingKey可以指定通配符
#：代表0个或多个词
*：代表1个词




消息转换器
Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter，基于JDK的ObjectOutputStream完成序列化。
如果要修改只需要定义一个MessageConverter 类型的Bean即可。推荐用JSON方式序列化，步骤如下：

我们在publisher服务引入依赖
<!--jackson-->
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.10</version>
</dependency>

我们在publisher服务声明MessageConverter：
@Bean//手动声明了Bean对象,将会代替Spring自动配置中默认生成的Bean对象
public MessageConverter messageConverter(){
    return new Jackson2JsonMessageConverter();
}


consumer服务中要想接收json格式的数据也需要相同操作
然后定义一个消费者，监听object.queue队列并消费消息：
@RabbitListener(queues = "object.queue")
public void listenObjectQueue(Map<String, Object> msg) {
    System.out.println("收到消息：【" + msg + "】");
}
//使用了自定义的MessageConverter转换器后发消息的时候是什么类型,监听回调函数形参就用什么类型接收消息


总结
SpringAMQP中消息的序列化和反序列化是怎么实现的？
利用MessageConverter实现的，默认是JDK的序列化,会造成消息没有可读性,且冗长,一般使用自定义的消息转换器来将Java Object转成Json格式再发送消息,接收方再以相同的MessageConverter将Json格式的消息转回原格式,因而使用了自定义的MessageConverter转换器后发消息的时候是什么类型,监听回调函数形参就用什么类型接收消息,即发送方与接收方必须使用相同的MessageConverter




*/

    /**
     * RabbitMQ高级
     *
     * MQ的一些常见问题
     * 1.消息可靠性问题 如何确保发送的消息至少被消费一次
     * 2.延迟消息问题   如何实现消息的延迟投递
     * 3.消息堆积问题   如何解决数百万消息堆积，无法及时消费的问题
     * 4.高可用问题     如何避免单点的MQ故障而导致的不可用问题
     *
     *
     * 消息可靠性问题:
     * 消息从生产者发送到exchange，再到queue，再到消费者，有哪些导致消息丢失的可能性？
     * 1.发送时丢失：
     *      生产者发送的消息未送达exchange
     *      消息到达exchange后未到达queue
     * 2.MQ宕机，queue将消息丢失
     * 3.consumer接收到消息后未消费就宕机
     *
     *
     *生产者确认机制(生产者可以知悉发送是否成功):
     *
     * RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后，会返回一个结果给发送者，表示消息是否处理成功。结果有两种请求：
     * publisher-confirm，发送者确认
     * 消息成功投递到交换机，返回ack
     * 消息未投递到交换机，返回nack
     * publisher-return，发送者回执
     * 消息投递到交换机了，但是没有路由到队列。返回ACK，及路由失败原因。
     * 注意 : 确认机制发送消息时，需要给每个消息设置一个全局唯一id，以区分不同消息，避免ack冲突
     *
     *
     *SpringAMQP实现生产者确认
     * 1.修改配置
     * 在publisher这个微服务的application.yml中添加配置：
     * spring:
     *      rabbitmq:
     *          publisher-confirm-type: correlated
     *          publisher-returns: true
     *          template:
     *              mandatory: true
     * 配置说明：
     * ◉ publish-confirm-type：开启publisher-confirm，这里支持3种类型：
     *      ◉ none: 不做生产者确认
     *      ◉ simple：同步等待confirm结果，直到超时
     *      ◉ correlated：异步回调，定义ConfirmCallback，MQ返回结果时会回调这个ConfirmCallback
     * ◉ publish-returns：true 开启publish-return功能，同样是基于callback机制，不过是定义ReturnCallback
     * ◉ template.mandatory：定义消息路由失败时的策略。true，则调用ReturnCallback；false：则直接丢弃消息
     *
     * 2.定义Return回调
     * 每个RabbitTemplate只能配置一个ReturnCallback，因此需要在项目加载时配置：
     * 修改publisher服务，添加一个配置类：
     * package cn.itcast.mq.config;
     * import lombok.extern.slf4j.Slf4j;
     * import org.springframework.amqp.rabbit.core.RabbitTemplate;
     * import org.springframework.beans.BeansException;
     * import org.springframework.context.ApplicationContext;
     * import org.springframework.context.ApplicationContextAware;
     * import org.springframework.context.annotation.Configuration;
     *
     * @Slf4j
     * @Configuration
     * public class CommonConfig implements ApplicationContextAware {
     *     @Override
     *     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
     *         // 获取RabbitTemplate
     *         RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
     *         // 设置ReturnCallback
     *         rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
     *             // 投递失败，记录日志
     *             log.info("消息发送失败，应答码{}，原因{}，交换机{}，路由键{},消息{}",replyCode, replyText, exchange, routingKey, message.toString());
     *             // 如果有业务需要，可以重发消息
     *         });
     *     }
     * }
     *
     * 3.定义Confirm回调
     * ConfirmCallback可以在发送消息时指定，因为每个业务处理confirm成功或失败的逻辑不一定相同。
     * 在publisher服务的cn.itcast.mq.spring.SpringAmqpTest类中，定义一个单元测试方法：
     *
     * public void testSendMessage2SimpleQueue() throws InterruptedException {
     *     // 1.消息体
     *     String message = "hello, spring amqp!";
     *     // 2.全局唯一的消息ID，需要封装到CorrelationData中,对应配置文件中的publisher-confirm-type: correlated
     *     CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
     *     // 3.添加callback
     *     correlationData.getFuture().addCallback(
     *         result -> {
     *             if(result.isAck()){
     *                 // 3.1.ack，消息成功
     *                 log.debug("消息发送成功, ID:{}", correlationData.getId());
     *             }else{
     *                 // 3.2.nack，消息失败
     *                 log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
     *             }
     *         },
     *         ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
     *     );
     *     // 4.发送消息
     *     rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData);
     *     //                          交换机名称,routingkey名称,消息,相关数据(全局唯一的消息ID + 消息发送结果的callback函数)
     *     // 休眠一会儿，等待ack回执
     *     Thread.sleep(2000);
     * }
     *
     *
     * 总结:
     * SpringAMQP中处理消息确认的几种情况：
     * publisher-comfirm：
     * 消息成功发送到exchange，返回ack,调用confirm回调函数
     * 消息发送失败，没有到达交换机，返回nack,调用confirm回调函数
     * 消息发送过程中出现异常，没有收到回执,调用confirm回调函数
     * publisher-return:
     * 消息成功发送到exchange，但没有路由到queue，调用Return回调函数
     *
     *
     *
     *
     *
     * 消息持久化:
     *
     * MQ默认是内存存储消息，开启持久化功能可以确保缓存在MQ中的消息不丢失。
     * 交换机持久化：
     * @Bean
     * public DirectExchange simpleExchange(){
     *      // 三个参数：交换机名称、交换机是否持久化、当没有queue与其绑定时是否自动删除
     *      return new DirectExchange("simple.direct"交换机名称, true交换机是否持久化, false当没有queue与其绑定时是否自动删除);
     * }
     * 队列持久化：
     * @Bean
     * public Queue simpleQueue(){
     *      // 使用QueueBuilder构建队列，durable就是持久化的
     *      return QueueBuilder.durable("simple.queue").build();
     * }
     * 消息持久化，SpringAMQP中的的消息默认是持久的，可以通过MessageProperties中的DeliveryMode来指定的：
     * Message msg = MessageBuilder
     * .withBody(message.getBytes(StandardCharsets.UTF_8)) // 消息体
     * .setDeliveryMode(MessageDeliveryMode.PERSISTENT) // 持久化
     * .build();
     *
     * 注意:Spring中 交换机 队列 消息 本身就默认是持久化的
     *
     *
     *
     *
     * 消费者确认:
     *
     * RabbitMQ支持消费者确认机制，即：消费者处理消息后可以向MQ发送ack回执，MQ收到ack回执后才会删除该消息。而SpringAMQP则允许配置三种确认模式：
     * 1.manual：手动ack，需要在业务代码结束后，调用api发送ack。
     * 2.auto：自动ack，由spring监测listener代码是否出现异常，没有异常则返回ack；抛出异常则返回nack,通常使用auto消费者确认模式
     * 3.none：关闭ack，MQ假定消费者获取消息后会成功处理，因此消息投递后队列中立即删除
     * 配置方式是修改application.yml文件，添加下面配置：
     * spring:
     *  rabbitmq:
     *   listener:
     *    simple:
     *     prefetch: 1
     *     acknowledge-mode: none # none，关闭ack；manual，手动ack；auto：自动ack
     *
     *
     * 消费者失败重试:
     *
     * 当消费者出现异常后，消息会不断requeue（重新入队）到队列，再重新发送给消费者，然后再次异常，再次requeue，无限循环，导致mq的消息处理飙升，带来不必要的压力：

     * 我们可以利用Spring的retry机制，在消费者出现异常时利用本地重试，而不是发送ack/nack无限制的requeue到mq队列。
     * spring:
     *      rabbitmq:
     *          listener:
     *              simple:
     *                  prefetch: 1
     *                  retry:
     *                      enabled: true # 开启消费者失败重试
     *                          initial-interval: 1000 # 初始的失败等待时长为1秒
     *                          multiplier: 1 # 下次失败的等待时长倍数，下次等待时长 = multiplier * last-interval
     *                          max-attempts: 3 # 最大重试次数
     *                          stateless: true # true无状态；false有状态。如果业务中包含事务，这里改为false
     *                          # max-interval: 10000 #毫秒，可以限制最大等待间隔时长
     *
     * 尝试次数耗尽后,消费者默认将返回reject,队列消息被直接丢弃
     *
     *
     * 消费者失败消息处理策略:
     *
     * 在开启重试模式后，重试次数耗尽，如果消息依然失败，则需要有MessageRecoverer接口来处理，它包含三种不同的实现：
     * RejectAndDontRequeueRecoverer：重试耗尽后，直接reject，丢弃消息。默认就是这种方式
     * ImmediateRequeueMessageRecoverer：重试耗尽后，返回nack，消息重新入队
     * RepublishMessageRecoverer：重试耗尽后，将失败消息投递到指定的交换机
     *
     * RepublishMessageRecoverer处理模式：
     *
     * 首先，定义接收失败消息的交换机、队列及其绑定关系：
     * @Bean
     * public DirectExchange errorMessageExchange(){
     *      return new DirectExchange("error.direct");
     * }
     * @Bean
     * public Queue errorQueue(){
     *      return new Queue("error.queue", true交换机是否持久化);
     * }
     * @Bean
     * public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
     *     return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("errorRoutingKey");
     * }
     *
     * 然后，定义RepublishMessageRecoverer：
     * @Bean
     * public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
     *      return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
     * }
     *
     *
     * ###总结,面试题:
     * 如何确保RabbitMQ消息的可靠性？
     * 🔴开启生产者确认机制，确保生产者的消息能到达队列
     * 🔴开启持久化功能，确保消息未消费前在队列中不会丢失
     * 🔴开启消费者确认机制为auto，由spring确认消息处理成功后返回ack给MQ,失败返回nack
     * 🔴开启消费者本地失败重试机制，并设置RepublishMessageRecoverer，多次重试失败后将消息投递到异常交换机，交由人工处理
     *
     *
     *
     *
     * 死信交换机
     * 当一个队列中的消息满足下列情况之一时，可以成为死信（dead letter）：
     * 1.消费者使用basic.reject或 basic.nack声明消费失败，并且消息的requeue参数设置为false
     * 2.消息是一个过期消息，超时无人消费
     * 3.要投递的队列消息堆积满了，最早的消息可能成为死信
     * 如果该队列配置了dead-letter-exchange = dl.direct 和 dead-letter-routing-key = dl属性，指定了一个交换机，那么队列中的死信就会投递到这个交换机中，再进入死信队列,而这个交换机就称为死信交换机（Dead Letter Exchange，简称DLX）。
     *
     * 死信交换机的作用:
     * 1.可以像上文提到的消费者消费消息失败时RepublishMessageRecoverer将消息重新加入到'异常交换机'一样,实现异常消息的兜底方案
     * 2.还可以处理超时消息...
     *
     *
     * 死信交换机实现延迟消息
     * TTL，也就是Time-To-Live。如果一个队列中的消息TTL结束仍未消费，则会变为死信，ttl超时分为两种情况：
     * 1.消息所在的队列设置了存活时间 x-message-ttl = 10000
     * 2.消息本身设置了存活时间 ttl = 5000
     * 较短者生效
     *
     * 消费者Listener类中
     * 我们声明一组死信交换机和队列，基于注解方式：
     * @RabbitListener(bindings = @QueueBinding(
     *      value = @Queue(name = "dl.queue", durable = "true"),
     *      exchange = @Exchange(name = "dl.direct"),
     *      key = "dlRoutingKey"
     * ))
     * public void listenDlQueue(String msg){
     *      log.info("接收到 dl.queue的延迟消息：{}", msg);
     * }
     *
     * Config类中
     * 要给队列设置超时时间，需要在声明队列时配置x-message-ttl属性：
     * @Bean
     * public DirectExchange ttlExchange(){
     *      return new DirectExchange("ttl.direct");
     * }
     * @Bean
     * public Queue ttlQueue(){
     *      return QueueBuilder
     *      .durable("ttl.queue") // 指定队列名称，并持久化
     *      .ttl(10000) // 设置队列中消息的超时时间，10秒
     *      .deadLetterExchange("dl.direct") // 指定死信交换机
     *      .deadLetterRoutingKey("dl") // 指定死信RoutingKey
     *      .build();
     * }
     * @Bean
     * public Binding ttlBinding(DirectExchange ttlExchange,Queue ttlQueue){
     *      return BindingBuilder.bind(ttlQueue).to(ttlExchange).with("ttlRoutingKey");
     * }
     * ↑写法等价↓
     * @Bean
     * public Binding ttlBinding(){
     *         return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttlRoutingKey");
     *     }
     *
     *
     *
     * 也可以发送消息时，给消息本身设置超时时间
     * @Test
     * public void testTTLMsg() {
     *      // 创建消息
     *     Message message = MessageBuilder
     *          .withBody("hello,ttl message".getBytes(StandardCharsets.UTF_8))
     *          .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
     *          .setExpiration("5000") //5秒过期
     *          .build();
     *     // 发送消息
     *     rabbitTemplate.convertAndSend("ttl.direct", "ttlRoutingKey", message, correlationData);
     * }
     *
     *
     *
     * 延迟队列
     * 利用TTL结合死信交换机，我们实现了消息发出后，消费者延迟收到消息的效果。这种消息模式就称为延迟队列（Delay Queue）模式。
     * 延迟队列的使用场景包括：
     * 延迟发送短信
     * 用户下单，如果用户在15 分钟内未支付，则自动取消
     * 预约工作会议，20分钟后自动通知所有参会人员
     *
     *
     * 因为延迟队列的需求非常多，所以RabbitMQ的官方也推出了一个插件，原生支持延迟队列效果。
     * 详细安装过程参考课前资料文档《RabbitMQ部署指南.md》中的第2节《安装DelayExchange插件》：
     *
     *
     *
     * ### DelayExchange原理
     *
     * DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时，流程如下：
     *
     * - 接收消息
     * - 判断消息是否具备x-delay属性
     * - 如果有x-delay属性，说明是延迟消息，持久化到硬盘，读取x-delay值，作为延迟时间
     * - 返回routing not found结果给消息发送者
     * - x-delay时间到期后，重新投递消息到指定队列
     *
     *
     * SpringAMQP使用延迟队列插件
     * DelayExchange的本质还是官方的三种交换机，只是添加了延迟功能。因此使用时只需要声明一个交换机，交换机的类型可以是任意类型，然后设定delayed属性为true即可。
     * 基于注解方式（推荐）:
     * @RabbitListener(bindings = @QueueBinding(
     *             value = @Queue(name = "dl.queue"),
     *             exchange = @Exchange(name = "dl.direct",delayed = "true"),//不写type默认是direct交换机
     *             key = "dlRoutingKey"
     *     ))
     * 也可以基于@Bean的方式：
     * @Bean
     * public DirectExchange delayedExchange(){
     *     return ExchangeBuilder
     *          .directExchange("delay.direct") //指定交换机类型和名称
     *          .delayed() //设置delay属性为true
     *          .durable(true) //持久化
     *          .build();
     * }
     * 然后我们向这个delay为true的交换机中发送消息，一定要给消息添加一个header：x-delay，值为延迟的时间，单位为毫秒：
     * Message message = MessageBuilder
     *                 .withBody("hello,ttl message".getBytes(StandardCharsets.UTF_8))
     *                 .setHeader("x-delay",10000)
     *                 .build();
     *
     * #总结
     * 延迟队列插件的使用步骤包括哪些？
     * 声明一个交换机，添加delayed属性为true
     * 发送消息时，添加x-delay头，值为超时时间
     *
     *
     *
     *
     *
     * 消息堆积问题
     * 当生产者发送消息的速度超过了消费者处理消息的速度，就会导致队列中的消息堆积，直到队列存储消息达到上限。最早接收到的消息，可能就会成为死信，会被丢弃，这就是消息堆积问题。
     *
     * 解决消息堆积有三种种思路：
     * 1.增加更多消费者，提高消费速度,也就是我们之前说的work queue模式
     * 2.在消费者内开启线程池加快消息处理速度
     * 3.扩大队列容积，提高堆积上限
     *
     *
     * 惰性队列---扩大队列容积
     * 从RabbitMQ的3.6.0版本开始，就增加了Lazy Queues的概念，也就是惰性队列。
     * 惰性队列的特征如下：
     * 1.接收到消息后直接存入磁盘而非内存
     * 2.消费者要消费消息时才会从磁盘中读取并加载到内存
     * 3.支持数百万条的消息存储
     *
     * 当队列堆积比较严重,可以将其设置为惰性队列,而要设置一个队列为惰性队列，只需要在声明队列时，指定x-queue-mode属性为lazy即可。
     *
     * 也可以通过命令行将一个运行中的队列修改为惰性队列：
     * rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
     * 命令解读：
     * - `rabbitmqctl` ：RabbitMQ的命令行工具
     * - `set_policy` ：添加一个策略
     * - `Lazy` ：策略名称，可以自定义
     * - `"^lazy-queue$"` ：用正则表达式匹配队列的名字
     * - `'{"queue-mode":"lazy"}'` ：设置队列模式为lazy模式
     * - `--apply-to queues  `：策略的作用对象，是所有的队列
     *
     * 用SpringAMQP声明惰性队列分两种方式：
     *
     * Bean配置类的方式：
     * @Bean
     * public Queue lazyQueue(){
     *     return QueueBuilder
     *          .durable("lazy.queue") // 指定队列名称，并持久化
     *          .lazy() //开启x-queue-mode为lazy
     *          .build();
     * }
     *
     * 注解方式：
     * @RabbitListener(queuesToDeclare=@Queue(
     *      name="lazy.queue",
     *      durable="true",
     *      arguments=@Argument(name="x-queue-mode",value="lazy")
     * ))
     * public void listenLazyQueue(String msg){
     *     System.err.println("接收到lazy.queue的消息:"+msg);
     * }
     *
     *
     * #总结
     * 消息堆积问题的解决方案？
     * 队列上绑定多个消费者，提高消费速度
     * 给消费者开启线程池，提高消费速度
     * 使用惰性队列，可以再mq中保存更多消息
     *
     * 惰性队列的优点有哪些？
     * 基于磁盘存储，消息上限高
     * 惰性队列直接写磁盘,不是先内存再磁盘,没有间歇性的page-out，性能比较稳定
     *
     * 惰性队列的缺点有哪些？
     * 基于磁盘存储，消息时效性会降低
     * 性能受限于磁盘的IO速率
     *
     *
     *
     *
     *
     * 4.MQ可用性问题------MQ集群
     *
     * 集群分类
     * RabbitMQ的是基于Erlang语言编写，而Erlang又是一个面向并发的语言，天然支持集群模式。RabbitMQ的集群有两种模式：
     * 普通集群：是一种分布式集群，将队列分散到集群的各个节点，从而提高整个集群的并发能力。
     * 镜像集群：是一种主从集群，普通集群的基础上，添加了主从备份功能，提高集群的数据可用性。
     *
     * 镜像集群虽然支持主从，但主从同步并不是强一致的，某些情况下可能有数据丢失的风险。因此在RabbitMQ的3.8版本以后，推出了新的功能：仲裁队列来代替镜像集群，底层采用Raft协议确保主从的数据一致性。
     *
     * 普通集群:
     *
     * 普通集群，或者叫标准集群（classic cluster），具备下列特征：
     * 会在集群的各个节点间共享部分数据，包括：交换机、队列元信息(队列的引用),队列本身仅存在于某个节点,队列中的消息自然也不共享
     * 当访问集群某节点时，如果队列不在该节点，会从数据所在节点传递到当前节点并返回
     * 队列所在节点宕机，队列中的消息就会丢失
     *
     * 镜像集群:
     *
     * 镜像集群：本质是主从模式，具备下面的特征：
     * 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份。
     * 创建队列的节点被称为该队列的主节点，备份到的其它节点叫做该队列的镜像节点。
     * 有多个队列q1 q2 q3时,一个队列的主节点可能是另一个队列的镜像节点
     * 所有操作(发送,监听,消费...)都是主节点完成,即便是从节点接收到请求,也会路由到主节点去完成,然后同步到从节点,镜像节点仅仅起到备份数据作用。
     * 当主节点接收到消费者的ACK时，所有镜像都会删除节点中的数据。
     * 主宕机后，镜像节点会替代成新的主节点
     *
     * 仲裁队列:
     *
     * 仲裁队列：仲裁队列是3.8版本以后才有的新功能，用来替代镜像队列，具备下列特征：
     * 与镜像队列一样，都是主从模式，支持主从数据同步(默认是1主4从)
     * 使用非常简单，没有复杂的配置
     * 主从同步基于Raft协议，强一致
     *
     * SpringAMQP创建仲裁队列：
     * @Bean
     * public Queue quorumQueue() {
     *      return QueueBuilder
     *      .durable("quorum.queue") // 持久化
     *      .quorum() // 仲裁队列
     *      .build();
     * }
     * SpringAMQP连接集群，只需要在消费者的yaml中配置即可：
     * spring:
     *      rabbitmq:
     *          addresses: 192.168.150.105:8071, 192.168.150.105:8072, 192.168.150.105:8073 #ip:port的格式指定多个MQ服务器
     *          username: itcast
     *          password: 123321
     *          virtual-host: /
     *
     *
     *
     * 具体操作见参考课前资料：《RabbitMQ部署指南.md》or Tlias视频
     *
     */
}
